/*
Copyright (c) 2008  Franklin Schmidt <fschmidt@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package fschmidt.util.java;

/**
 * Base64 encoding and decoding (MIME spec RFC 1521).
 */
public class Base64 {

	/**
	 * Stand alone test
	 */
	public static void main(String[] args) 
		throws Exception {
		if (args.length < 2) {
			System.out.println("Usage: Base64 <encode/decode> <inString>");
			System.exit(0);
		}
		
		String inString = args[1]; 
		System.out.println("InString = " + inString);
		
		if (args[0].equalsIgnoreCase("encode")) {
		       
		    String encodedString = Base64.encode(inString.getBytes());
		    System.out.println("EncodedString = " + encodedString);    
		    
		} else {
            String outString = new String(Base64.decode(inString));		
		    System.out.println("DecodedString = " + outString);
		}	
	}
	
	/**
	 * Base64-encode binary data.
	 *
	 * @param bytes A buffer holding the data to encode.
	 * @return String String containing the encoded data
	 */
	public static String encode(byte[] bytes) {
		return encode(bytes, false);
	}

	/**
	 * base64-encode binary data.
	 *
	 * @param bytes A buffer holding the data to encode.
	 * @param lineBreaks true if the encoded data will be broken
	 *				  into 64-character lines with CRLF pairs.
	 * @return	String String containing the encoded data
	 */
	public static String encode(byte[] bytes, 
								boolean lineBreaks) {

		/* slightly bigger buffer than we usually need */
		StringBuilder buf= new StringBuilder((int)(bytes.length * 1.4));

    	int temp			= 0;	/* holder for the current group of 24 bits */
    	int charsWritten	= 0;	/* count of char written (to current line if lineBreaks == true) */
    	int bytesRead		= 0;	/* count of bytes read in the current 24-bit group */
    	for(int index = 0; index < bytes.length; ++index) {
    		
			temp <<= 8;
			temp |= (((int)bytes[index]) & EIGHT_BITS);
			++bytesRead;
			if(bytesRead == 3) {
				/* The lower 3 bytes of temp now hold the 24 bits of a 
				 * translation group. Write them out as 4 characters 
				 * that represent 6 bits each.
				 */
				buf.append(valueToChar[(temp >> 18) & SIX_BITS]);
				buf.append(valueToChar[(temp >> 12) & SIX_BITS]);
				buf.append(valueToChar[(temp >>  6) & SIX_BITS]);
				buf.append(valueToChar[(temp >>  0) & SIX_BITS]);
	    		temp = 0;
	    		bytesRead = 0;
	    		charsWritten += 4;
   	    		if(lineBreaks && charsWritten >= 64) {
					buf.append(CRLF);
					charsWritten = 0;
				}
	    	}
		}

		// The input byte[] is exhaused.  
		// If there were fewer than 3 bytes in the last translation group,
		// we must complete the output and pad it to a multiple of 4 chars.
		
    	switch(bytesRead){
			case 1:
				// The low 8 bits of temp take 2 characters to represent. 
	    		buf.append(valueToChar[(temp >> 2) & SIX_BITS]);
	    		buf.append(valueToChar[(temp << 4) & SIX_BITS]);
				buf.append('='); // padding 
				buf.append('='); // padding 
				if(lineBreaks) {
					buf.append(CRLF);
				}
	    		break;

			case 2:
				// The low 16 bits of temp take 3 characters to represent 
	    		buf.append(valueToChar[(temp >> 10) & SIX_BITS]);
	    		buf.append(valueToChar[(temp >>  4) & SIX_BITS]);
	    		buf.append(valueToChar[(temp <<  2) & SIX_BITS]);
				buf.append('='); // padding 
				if(lineBreaks){
					buf.append(CRLF);
				}
	    		break;

			default:
				// There were exactly 3 bytes in the last group. No padding needed. 
	    		break;
		}
	    if(charsWritten > 0 && lineBreaks) {
			buf.append(CRLF);
		}
		return buf.toString();
	}

	/**
	 * Decode a base64-encoded string into binary data.<P>
	 * 
	 * Ignores (skips over) carriage-return and line-feed characters.
	 * Stops decoding when it encounters the base64 pad character ('=')
	 * or when characters are encountered that are not MIME/base64 numerals.
	 * 
	 * @param		mimeStr a String containing the base64-encoded data
	 * @return		a byte[] containing the decoded binary data.
	 * @exception	RuntimeException if the mimeStr parameter violates 
	 *					the base64 encoding rules.
	 */
	public static byte[] decode(String mimeStr) throws RuntimeException {

		int		inputLength		= mimeStr.length();
		byte	decoded[]		= new byte[inputLength];
		int		temp			= 0;	// accumulator for 24-bit translation group 
		int 	charsRead		= 0;	// characters read in current translation group 
		int		bytesWritten	= 0;	// count of bytes written into decoded[] 
		char	currChar;				// current character from mimeStr 
		byte	currByte;				// base64 value represented by currChar 
		int		equalCount		= 0;	// The number of '=' characters we've encountered. 

		for (int i = 0; i < inputLength; ++i) {
			currChar = mimeStr.charAt(i);
			if (currChar == CR || currChar == LF) {
				//Skip over carriage return or line-feed character. 
				continue;
			}
			try {
				currByte = charToValue[(byte)currChar];
			} catch(ArrayIndexOutOfBoundsException obe) {
				currByte = BAD_VAL;
			}
			if (currByte == BAD_VAL) {
				if (charsRead == 0) {
					 /*
					  * First character of a translation group is a 
					  * non-base64 character.  We have presumably 
					  * reached the end of base64 encoded data. 
					  */
					break;
				} else if (currChar == '=' & charsRead >= 2) {
					// Only the 3d and 4th characters may be '=' 
					currByte = 0;
					++equalCount;
				} else {
					//String[] errArgs = new String[]{"Base64"};
					throw new InvalidCharEncodedStringException(); //RuntimeException("E_INVALID_CHAR_ENCODED_STRING");
				}
			}
			temp <<= 6;
			temp |= currByte;
			++charsRead;
			if (charsRead == 4) {
				/* 
				 * temp has all 24 bits of the translation group, 
				 * write the bytes to decode[]
				 */
				decoded[bytesWritten++]= (byte)(temp >> 16);
				if(equalCount < 2) {
					decoded[bytesWritten++]= (byte)(temp >> 8);
					if(equalCount < 1) {
						decoded[bytesWritten++]= (byte)(temp >> 0);
					}
				}
				charsRead = 0;
				temp = 0;
				if (equalCount > 0) {
					// Encoded data is not permitted after an '=', we must be done. 
					break;
				}
			}
		}	// end for == end of string 

		// Trim array to exact length 
		byte result[] = new byte[bytesWritten];
		System.arraycopy(decoded, 0, result, 0, bytesWritten);
		return result;
	}

	private static final byte BAD_VAL = (byte)0xFF;

	private static final int SIX_BITS = 0x3F;
	private static final int EIGHT_BITS = 0xFF;

	private static final char CR = '\r';
	private static final char LF = '\n';

	private static final String CRLF = "\r\n";

	/**
	 * Value to be encoded is an index into this array, so the 
	 * character representation of byte value = valueToChar[value]
	 */
	private static final char valueToChar[] = {
		'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 
		'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 
		'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 
		'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 
		'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 
		'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 
		'w', 'x', 'y', 'z', '0', '1', '2', '3', 
		'4', '5', '6', '7', '8', '9', '+', '/'
	};

	/**
	 * The ASCII value of a character is an index into this array, so that 
	 * decoded byte value = charToValue[(byte)(character)]. <P>
	 * 
	 * Characters that are not valid base64 numerals are detected
	 * one of two ways: either the expression above yeilds BAD_VAL
	 * or the array access gives an ArrayIndexOutOfBoundsException. <P>
	 *
	 */
	private static final byte charToValue[] = {
		BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	
		BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	
		BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	
		BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	
		BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	
		BAD_VAL,	BAD_VAL,	BAD_VAL,	62,			BAD_VAL,	BAD_VAL,	BAD_VAL,	63,	
		52,			53,			54,			55,			56,			57,			58,			59,	
		60,			61,			BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	
		BAD_VAL,	0,			1,			2,			3,			4,			5,			6,	
		7,			8,			9,			10,			11,			12,			13,			14,	
		15,			16,			17,			18,			19,			20,			21,			22,	
		23,			24,			25,			BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	
		BAD_VAL,	26,			27,			28,			29,			30,			31,			32,	
		33,			34,			35,			36,			37,			38,			39,			40,	
		41,			42,			43,			44,			45,			46,			47,			48,	
		49,			50,			51,			BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL,	BAD_VAL
	};

	public static final class InvalidCharEncodedStringException extends RuntimeException {}

}
